from datetime import datetime
import backtrader as bt
import matplotlib.pyplot as plt
import akshare as ak
import pandas as pd
from datetime import datetime, timedelta

from backtrader_plotting import Bokeh
from backtrader_plotting.schemes import Tradimo

plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False

# 获取股票后复权数据
#df = ak.stock_zh_a_hist(symbol="000001", adjust="hfq").iloc[:, :6]  #深圳股票
df= ak.fund_etf_hist_em(symbol="510500", adjust="").iloc[:, :6]  #etf


# 处理字段命名
df.columns = ['date', 'open', 'close', 'high', 'low', 'volume']
df['date'] = pd.to_datetime(df['date'])

# 设置回测时间范围
start_date = datetime(2023, 6, 3)
end_date = datetime(2024, 5, 27)
# 转换为Backtrader数据源  
data = bt.feeds.PandasData(  
    dataname=df, 
    fromdate=start_date, 
    todate=end_date, 
    datetime=0,  # 日期列索引，根据akshare返回的数据结构调整  
    open=1,      # 开盘价列索引  
    high=2,      # 最高价列索引  
    low=3,       # 最低价列索引  
    close=4,     # 收盘价列索引  
    volume=5,    # 成交量列索引  
    #openinterest=-1  # 若没有持仓量数据，则设置为-1  
)  
  

class SingleTestStrategy(bt.Strategy):
    params = (
        ('maperiod', 20),
    )

    def __init__(self):
        self.order = None
        self.sma = bt.ind.SMA(self.data, period=self.p.maperiod)
        pass

    def downcast(self, amount, lot):
	#使用整数除法 amount // lot 来找到 amount 可以被 lot 整除的最大次数。
        return abs(amount // lot * lot) 
        """ 	   
		self 的出现意味着这个函数实际上是类的一个方法，而不是一个独立的函数。
		class MyClass:  
 		   def downcast(self, amount, lot):  
  		      return abs(amount // lot * lot)  
  
		# 使用方法  
		my_instance = MyClass()  
		result = my_instance.downcast(123, 10)  
		print(result)  # 输出 120
	    """

    # 可以不要，但如果你数据未对齐，需要在这里检验
    # def prenext(self):
    #     print('prenext 执行 ', self.datetime.date(), self.getdatabyname('300015')._name
    #           , self.getdatabyname('300015').close[0])
    #     pass

    def next(self):
        # 检查是否有指令执行，如果有则不执行这bar
        if self.order:
            return
        # 回测如果是最后一天，则不进行买卖
        if pd.Timestamp(self.data.datetime.date(0)) == end_date:
            return
        if not self.position:  # 没有持仓
            # 执行买入条件判断：收盘价格上涨突破20日均线；
            # 不要在股票剔除日前一天进行买入
            if self.datas[0].close > self.sma and pd.Timestamp(self.data.datetime.date(1)) < end_date:
                # 永远不要满仓买入某只股票
                order_value = self.broker.getvalue() * 0.98
                order_amount = self.downcast(order_value / self.datas[0].close[0], 100)
                self.order = self.buy(self.datas[0], order_amount, name=self.datas[0]._name)

        else:
            # 执行卖出条件判断：收盘价格跌破20日均线，或者股票剔除
            if self.datas[0].close < self.sma or pd.Timestamp(self.data.datetime.date(1)) >= end_date:
                # 执行卖出
                self.order = self.order_target_percent(self.datas[0], 0, name=self.datas[0]._name)
                self.log(f'卖{self.datas[0]._name},price:{self.datas[0].close[0]:.2f},pct: 0')
        pass

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # Check if an order has been completed
        # Attention: broker could reject order if not enough cash
        if order.status in [order.Completed, order.Canceled, order.Margin]:
            if order.isbuy():
                self.log(
                    f"买入{order.info['name']}, 成交量{order.executed.size}，成交价{order.executed.price:.2f} 订单状态：{order.status}")
                self.log('买入后当前资产：%.2f 元' % self.broker.getvalue())
            elif order.issell():
                self.log(
                    f"卖出{order.info['name']}, 成交量{order.executed.size}，成交价{order.executed.price:.2f} 订单状态：{order.status}")
                self.log('卖出后当前资产：%.2f 元' % self.broker.getvalue())
            self.bar_executed = len(self)

        # Write down: no pending order
        self.order = None

    def log(self, txt, dt=None):
        """
        输出日期
        :param txt:
        :param dt:
        :return:
        """
        dt = dt or self.datetime.date(0)  # 现在的日期
        print('%s , %s' % (dt.isoformat(), txt))

    pass

    def notify_trade(self, trade):
        '''可选，打印交易信息'''
        pass

#===================================================================
cerebro = bt.Cerebro()



# 加载数据到Cerebro
# data_feed = bt.feeds.PandasData(dataname=df, fromdate=start_date, todate=end_date)
cerebro.adddata(data)

cerebro.broker.setcash(100000000.0)
cerebro.broker.setcommission(commission=0.001)
cerebro.addstrategy(SingleTestStrategy, maperiod=20)
cerebro.run()
cerebro.plot()

# b=Bokeh(style='bar',tabs='multi',scheme=Tradimo())# 传统白底，多页
# cerebro.plot(b)

if __name__ == '__main__':
    pass
